Smali
smali/baksmali是Android的Java VM实现dalvik使用的dex格式的汇编程序/反汇编程序。 语法松散地基于Jasmin/ dedexer的语法,并支持dex格式的全部功能(注释,调试信息,行信息等)
BuildProcedure(构建程序)
依赖
这可能是一份不完整的清单。如果您遇到遗漏的内容,请随时发表评论,我会将其添加
- jdk(8)
- git
其他所有内容都应该通过gradle / gradlew下载
构建
1 | git clone https://github.com/JesusFreke/smali.git |
(windows上,使用gradlew.bat 代替./gradlew) 生成的jar包位置
1 | smali/build/libs/smali-<version>.jar |
除此之外,通过
1 | ./gradlew proguard |
可以构建出体积更小混淆后的jar文件
1 | smali/build/libs/smali-<version>-small.jar |
DeodexInstructions(Deodex说明)
说明
从v2.1.0开始,baksmali支持反编译ART oat文件。 支持的最低oat版本为56,也就是Android 6.0 / Marshmallow版本中的oat文件版本。 由于与字段排序有关的一些潜在问题,暂不支持M版本之前的oat文件反编译(例如Lollipop)。
你可以在oat文件上运行baksmali以获取其中所有odex文件的列表。
1 | > adb pull /system/framework/arm/boot.oat boot.oat |
通过如下操作我们可以反编译boot.oat中的framework jar文件
1 | > adb pull /system/framework/arm/boot.oat /tmp/framework/boot.oat |
或者通过如下方式反编译一个应用程序oat
1 | > adb pull /system/framework/arm/boot.oat /tmp/framework/boot.oat |
注意,在反编译oat文件时不需要-a选项来指定API Level,baksmali会自动使用oat文件中的API Level。
从v1.4.0开始,deodexing过程已经大大简化了,不再需要通过-c选项指定其他类路径。
Deodexing现在很简单
1 | baksmali -a <api_level> -x <odex_file> -d <framework_dir> |
odex文件常识
简而言之,odex文件就是classes.dex文件的优化版本,针对特定设备的优化。 值得一提的是,odex文件依赖于生成时加载的每个“BOOTCLASSPATH”文件。 odex文件仅在与这些确切的BOOTCLASSPATH文件一起使用时才有效。 dalvik通过存储odex文件所依赖的每个文件的校验和来强制执行此操作,并确保在加载odex文件时每个文件的校验和匹配。
除了加载的主apk / jar之外,BOOTCLASSPATH只是可以加载类的jars / apk列表而已。 一般android系统的基础BOOTCLASSPATH中包含5个jar包 - core.jar,ext.jar,framework.jar,android.policy.jar和services.jar。 这些都可以在/ system / framework中找到。 但是,有些apks依赖于额外的jar或apks文件。 例如,对于使用谷歌地图的应用程序,com.google.android.maps.jar将附加到该应用程序的apk的BOOTCLASSPATH中。
下面这几个由于odex的依赖性造成的问题有时会让事情变得比较麻烦。 首先 - 你不能从一个系统镜像中获取apk + odex文件并在另一个系统映像上运行它(除非其他系统映像使用完全相同的framework文件)。 另一个问题是,如果对任何BOOTCLASSPATH文件进行任何更改,它将使依赖于该文件的每个odex无效 - 基本上每个设备上的apk / jar都会失效。
Deodexing(反编译Odex)
以下示例默认你使用下载页面上提供的baksmali wrapper脚本来调用baksmali。 如果你是直接调用jar,可以将“baksmali”替换为“java -jar baksmali.jar”
deodexing的两个主要选项是-x和-d。 -x是告诉baksmali你想要deodex的对象,-d告诉baksmali从哪里加载依赖项。
依赖内容可以是odex文件的形式,也可以是包含classes.dex文件的jar / apk。两者皆可。
例如,你想从ICS镜像中反编译Calculator.odex文件并且所有的framework odex文件和jar包都放在一个名为framework的文件夹中,你就可以通过如下方式操作:
1 | baksmali -a 15 -x Calculator.odex -d framework -o Calculator |
反编译Calculator.odex,并将生成的deodexed smali文件放入名为Calculator的目录中。
Troubleshooting(问题解决)
Heap Space
你可能遇到的一个问题是它是否已用完堆内存。 该错误可能类似于“java.lang.OutOfMemoryError:Java heap space”。 要解决此问题,可以使用-Xmx参数来增加堆大小。 尝试将其设置为512m。 当然,如果需要,你可以进一步增加它。 如果你使用wrapper script来调用baksmali,则可以使用-JXmx。 例如:
1 | baksmali -JXmx512m -x blah.odex |
或者通过如下方式直接调用jar包
1 | java -Xmx512m -jar baksmali.jar -x blah.odex |
Registers(寄存器)
介绍
在dalvik字节码中,寄存器是32位,并且可以保存任何类型的值。 2个寄存器用于保存64位类型(Long和Double)。
置顶方法中寄存器数量
有两种方法可以指定方法中有多少可用寄存器。 .registers指令指定方法中寄存器的总数,而.locals指令指定方法中非参数寄存器的数量。 寄存器总数包括保存方法参数所需的寄存器。
方法参数如何传递至方法
调用方法时,方法的参数将会被放置到倒数的n个寄存器中。如果某个方法有2个参数,5个寄存器(v0-v4),此时参数会被放置到最后两个寄存器中,也就是v3和v4。
对于非static方法,第一个参数永远都是调用该方法的对象。
比如,非static方法LMyObject;->callMe(II)V.,这个方法有2个int型参数,但是还存在一个隐型的 LMyObject; 参数在两个整型参数之前,所以这个方法有3个参数。
假设通过.registers 5 指令或者.local 2指令(i.e. 2个local寄存器和3个参数寄存器)来指定这个方法有5个寄存器(v0 - v4),当方法被调用时,调用此方法的对象(也就是 this 引用)将会被放置到v2寄存器中,而第一个整型参数将会被放置到v3寄存器中,第二个整型参数会被放置到v4寄存器中。
对于static方法,只是少了一个隐型参数,this而已。
Register names(寄存器名称)
对于寄存器,有两种命名方案:普通的v开头命名方案和针对参数寄存器的p开头命名方案。我们继续看上面那个有3个参数,5个寄存器的方法,下面这个表格分别用两种命名方式来表示寄存器。
Local | Param | 数量 |
---|---|---|
v0 | 第一个local寄存器 | |
v1 | 第二个local寄存器 | |
v2 | p0 | 第一个参数寄存器 |
v3 | p1 | 第二个参数寄存器 |
v4 | p2 | 第三个参数寄存器 |
Motivation for introducing parameter registers(引入参数寄存器的原因)
p开头命名方案的引入是为了解决大家在编辑smali过程中的共同烦恼。比方说,你想添加一下代码到一个有几个参数的方法,然后你发现,你需要一个额外的寄存器。你可能会想“这不是小问题嘛,只需要在.registers指令上加一个不就完事了”。
显然,问题不可能那么简单。记住一点,方法参数是存放在可用寄存器中倒数的几个寄存器中。如果你增加寄存器的数量,你将会改变方法参数存放的寄存器。因此,你必须更改.registers指令并重新编号每个参数寄存器。
但是如果在整个方法中使用p命名方案引用参数寄存器,则可以轻松更改方法中的寄存器数量,而无需担心重新编号任何现有寄存器。
注意:默认情况下,baksmali将使用参数寄存器的p命名方案。如果由于某种原因要禁用此命令并强制baksmali始终使用v命名方案,则可以使用-p / - no-parameter-registers选项。
Long/Double值
如前所述,Long和Double类型(分别为J和D)是64位值,需要2个寄存器。在引用方法参数时要记住这一点。
比如,有一个非static方法
1 | LMyObject;->MyMethod(IJZ)V |
方法参数类型依次是:LMyObject;, int, long, bool。所以,这个方法需要5个寄存器来存放参数。
Register | Type |
---|---|
p0 | this |
p1 | I |
p2,p3 | J |
p4 | Z |
SmaliBaksmali2.2
v2.2版本后,smail和baksmali有了新的脚手架工具。
1 | baksmali disassemble app.apk -o app |
执行baksmali help 和 smali help 查看入门内容。
注意以下几点:
- 你可以把apk或者oat文件当成文件夹,单独指定其中的文件作为对象,就像这样
baksmali disassemble app.apk/classes2.dex
。执行baksmali help input
查阅更多相关信息。 - deodexing时指定bootclasspath/classpath的命令选项做了小的调整。现在deodexing oat 文件,一般这样操作
adb pull /system/framework framework && baksmali deodex app.odex -b framework/arm/boot.oat
- baksmail现在包含一系列列举dex/oat/apk 文件各种相关信息的命令
baksmali list dex boot.oat
列举出oat文件中所有dex(此命令也适用于apk文件)baksmali list classes app.apk
列举apk/dex等文件中的所有类baksmali list methods app.apk | wc -l
获取当前dex文件中的方法数
- V2.2支持在Nougat版本上解码oat文件,并包含一些针对Marshmallow版本的错误修正
- 在dexlib2等文件中有一些重大的API更改。因此,如果你打算将工具升级到dexlib2 v2.2,你可能需要花点事件去解决一些问题
SmaliBaksmali2.0
smali/baksmali 2.0版本是下一个重大更新版本,主要就是dexlib的更新。当前正处于测试阶段,如果想体验一下,可以自行前往下载界面下载2.0b1 jar文件。
这对你意味着什么?大部分内容对你没有影响,值得一提的是它运行更快并且使用更少的内存。
但是,下面这几点你还是需要稍微注意一下。
Multithreading(多线程)
smali和baksmali现在是多线程的!默认情况下,它们使用可用的CPU数,最多为6。你可以使用-j选项进行设置。
为了在具有多线程的smali中获得最佳性能,最好稍微提高内存。如果你正在使用smali wrapper script,则可以通过添加-JXmx512m选项手动执行此操作。
1 | smali -JXmx512m out -o classes.dex |
当然,你可以使用默认只位512m的smali wrapper script
Language Changes(语言改动)
我利用一次重大修订的机会来调整语言本身的一些内容。
.parameter -> .param
1 | .method parameters(IILjava/lang/String;)V |
变成
1 | .method parameters(IILjava/lang/String;)V |
.parameter指令现在为.param,它的工作方式与.local指令类似。 .param指令不是按顺序将.parameter信息与每个参数匹配,而是使用register参数指定与之关联的参数。
.array-data changes
1 | .array-data 0x4 |
变成
1 | .array-data 4 |
.array-data中的每个数字现在都是它自己的元素,而不是之前的语法,它会连接每个数字的little-endian编码,然后将完整的连接字节数组重新解释为n-byte little-endian序编码元素。
const/high16 and const-wide/high16
1 | const/high16 v0, 0x1234 |
变成
1 | const/high16 v0, 0x12340000 |
更改了* / high16指令的语法,以便它使用加载到寄存器中的实际值,而不仅仅是存储在指令中的16位。
也就是说指定除了顶部16位以外的位为非0是错误的。例如const/high16 v0, 0x12340001
将会是错误的。
smalidea
smalidea是一个针对Intellij IDEA以及Android Studio 的smali语言插件。
Download(下载)
当前为alpha版本,持续开发中。
Features(特性)
当前能力
- 语法高亮/语法错误
- 字节码级调试
- 断点
- 指令级单步调试
- 给任意寄存器添加watcher
- 调试过程中,完全java风格表达式本地窗口支持等
- 跳转到定义处
- 查找引用
- 重命名
- 在java代码中引用smali类(但是当前还不能真正被编译)
- 问题报告 - 从错误对话框轻松创建新的github问题
计划能力
- 自动补全(指令名称,类名/方法名/变量名等等)
- 纯Smali工程编译
- 健壮性错误检测(如,全字节码检测)
- 流畅地项目导入过程
- 自动检测源目录
- 选择SDK
- APK作为新项目导入引导
- 添加“Smali Class”到“New…”菜单中
- 在“locals”窗格中显示具有值的所有寄存器
- “watch”窗格中设置寄存器的值
有潜力功能
- smali+java混合工程编译
- “引入新寄存器”
- 导入或者deodex设备framework作为新模块或者作为sdk
- 公开寄存器类型分析数据
- 随时显示寄存器的预期类型
- 寻找寄存器设值位置
安装
- 下载最新smalidea zip 文件
- 在IDEA或者Android Studio中“Settings->Plugins”中”Install plugin from disk”
- “Apply”并且重启
调试
注意:单指令单步调试在 IDEA 14.1及以上版本支持(Android Studio是基于IDEA的,所以类似)。而此之前的版本,执行单步调试的时候,回调转到下一个.line指令,而不是下一条指令。
- 使用baksmali反编译一个应用到”src”文件夹,并将其作为一个新工程的源文件夹。
baksmali d myapp.apk -o ~/projects/myapp/src
- IDEA中,导入新项目,选择刚才的新工程目录
~/projects/myapp
- 在导入项目时使用“Create project from existing sources”选项
- 项目导入完成后,右键src文件夹,”Mark Directory As->Sources Root”
- 打开项目设置界面,选择或者创建合适的JDK
- 在设备上安装启动此应用
- 运行DDMS,找到对应的应用进程
- 在IDEA中,创建一个新的“Remote”调试配置(Run->Edit Configurations),并改变调试端口至8700
- Run -> Debug
- 当断点被触发时,应用程序将会暂停,此时你可以单步,添加watch等
或者 在Android Studio3.2下进行如下操作:
- 使用baksmali反编译一个应用到”src”文件夹,并将其作为一个新工程的源文件夹。
baksmali d myapp.apk -o ~/projects/myapp/src
- 在Android Studio中,关闭当前项目,执行“Open an existing Android Studio project”
- 项目创建完成后,右键src文件夹,”Mark Directory As->Sources Root”
- 确保应用AndroidManifest.xml中“android:debuggable=”true”。打开“USB调试”并且在“开发者选项”中使用“选择待调试应用”
- 启动应用程序并将JDWP服务转发到localhost,
adb forward tcp:8700 jdwp:$(timeout 0.5 adb jdwp | tail -n 1)
- 在Android Studio中,创建一个新的“Remote”调试配置(Run->Edit Configurations),并改变调试端口至8700
- 在Android Studio中, Run -> Debug
- 当断点被触发时,应用程序将会暂停,此时你可以单步,添加watch等
TypesMethodsAndField(类型,方法和字段)
类型
dalvik字节码有两个主要的类型,基本类型和引用类型。引用对象指的是对象,数组等基础类型外的所有类型。
每个基本类型都由单个字母代表。这并不是我想出来的点子,他们真真实实地以字符串的形式存储在dex文件中。它们在 dex-format.html 文档中被明确的指出来过(Android源码仓中dalvik/docs/dex-format.html)。
V | void - 只能用于返回类型 |
---|---|
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long(64bits) |
F | float |
D | double(64bits) |
Object则是以Lpackage/name/ObjectName;
的形式出现,L
表明这是一个对象类型,package/name/
指代包名,ObjectName
为当前对象类型名称,:
作为对象的结尾。这等价于java的package.name.ObjectName
,以String这个具体的类型为例子,Ljava/lang/String;
等价于java.lang.String
。
Arrays(数字)则是以[I
的形式出现,这指代的是一个int类型一维数组,也就是java中int[]
。至于多维数组,只需要在前面添加一个或者多个[
字符。[[I
=int[][]
,[[[I
=int[][][]
(注意,数字的最大维度为255)。
对象数据,如[Ljava/lang/String;
道理和上面是一样的。
方法
方法总是以非常详细的形式指定,包括方法的类型,方法名称,参数类型和返回类型。虚拟机需要所有这些信息才能找到正确的方法,并能够对字节码执行静态分析(用于验证/优化目的)。
一般形式如下:
1 | Lpackage/name/ObjectName;->MethodName(III)Z |
上面这个示例中,Lpackage/name/ObjectName;
表示类型,MethodName
表示方法名,(III)Z
中III
表示参数类型(3个int型),Z
表示方法的返回类型(bool类型)。
方法参数一个接一个地列出,它们之间没有分隔符。
下面有一个更复杂的示例:
1 | method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; |
等价于
1 | String method(int, int[][], int, String, Object[]) |
字段
字段同样总是以详细形式指定,包括包含字段的类型,字段名称和字段类型。同样,这些信息也是为了帮助虚拟机能够找到正确的字段,以及对字节码执行静态分析。
一般形式如下:
1 | Lpackage/name/ObjectName;->FieldName:Ljava/lang/String; |
这应该是不言自明的 - 它们分别是包名,字段名和字段类型。
UnresolvableOdexInstruction(暂时无法解决的Odex指令)
说明
在deodexing过程中,我们使用baksmali产生的一些smali文件中,你可能会注意到baksmali用一个throw或其他东西替换odexed指令的一些情况,其中的注释如“Replaced unresolvable optimized instruction with a throw”。
详情
比如,下面这段java代码:
1 | Object blah = null; |
相对应的Smali代码为:
1 | const v0, 0 |
这当然会导致空指针异常 - 但它是有效的代码。在实践中,这些案例更加隐蔽。
当代码被odexed时,invoke-virtual指令将被一个invoke-virtual-quick指令替换,如下所示:
1 | const v0, 0 |
其中vtable @ 7是指向Object是抽象方法表中toString()方法的索引。
但请注意,上面的代码并没有提到该方法所在的类。由于v0始终为null,并且我们只有vtable索引,因此baksmali不可能知道要查看哪个类。所以,不可能deodex 这条指令。但是,我们可以尽量对其进行处理:用具有完全相同效果的内容来替换这条指令。记住v0始终为null,所以通过它调用任何方法都会导致空指针。所以Samli就使用同样会抛出空指针的内容来替换这条无法解决的odex指令。
除此之外,紧随上面这些无法解决的odex指令的code,由于永远无法执行而成为了冗余代码(除非有其他分支可以访问到它们)。或者在一些其他情况下,代码依赖的是一个我们无法解决的odex指令方法结果,那么这部分代码也是不可能被deodex的,因为,这部分代码无法被访问到,全被移除或者注释掉了。
因此,简而言之,这些情况不应影响字节码的语义/功能。当你看到“replaced unresolvable optimized instruction”,不必大惊小怪。Baksmali对这部分呢日用的处理不影响代码功能/语义(如果对功能/语义造成了影响,那就是baksmali的bug)。
目前,存在与此相关的已知问题,其中如果try块中的所有代码都被注释掉,因为它依赖一个无法解析的odex指令,则(空)try块保留在其中,并且当安装到设备上时,空 try块会导致dalvik拒绝dex文件。